package com.katesoft.scale4j.persistent.spring;

import com.katesoft.scale4j.common.lock.IDistributedLockProvider;
import com.katesoft.scale4j.log.LogFactory;
import com.katesoft.scale4j.log.Logger;
import com.katesoft.scale4j.persistent.hibernate.EventListeners;
import org.hibernate.HibernateException;
import org.hibernate.cfg.Configuration;
import org.hibernate.cfg.Environment;
import org.hibernate.dialect.Dialect;
import org.hibernate.search.store.FSDirectoryProvider;
import org.hibernate.tool.hbm2ddl.DatabaseMetadata;
import org.springframework.dao.DataAccessException;
import org.springframework.jdbc.core.ConnectionCallback;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.init.DatabasePopulator;
import org.springframework.orm.hibernate3.annotation.AnnotationSessionFactoryBean;

import java.io.File;
import java.io.IOException;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.locks.Lock;

import static com.katesoft.scale4j.common.io.FileUtility.LINE_SEPARATOR;
import static com.katesoft.scale4j.common.io.FileUtility.path;
import static com.katesoft.scale4j.common.lang.RuntimeUtility.HIBERNATE_DATA_DIR;
import static com.katesoft.scale4j.common.lang.RuntimeUtility.HIBERNATE_SEARCH_DATA_DIR;
import static java.lang.Boolean.FALSE;
import static java.lang.Boolean.TRUE;
import static org.apache.commons.io.FileUtils.writeStringToFile;
import static org.apache.commons.lang.StringUtils.join;

/**
 * extended session factory bean.
 * <p/>
 * This class will create drop-create schema script as well as update schema script(if necessary) and drop schema script after building session factory.
 *
 * @author kate2007
 */
public class LocalAnnotationSessionFactory extends AnnotationSessionFactoryBean
{
    private static final String RTTP_DB_UPDATE_LOCK = "rttp.schema.creation.lock";
    private final Logger log = LogFactory.getLogger(LocalAnnotationSessionFactory.class.getName());
    private EventListeners eventListeners;
    private IDistributedLockProvider lockProvider;
    //
    private boolean generateSchemaUpdateScript;
    private boolean generateSchemaCreateScript;
    private boolean generateSchemaDropScript;
    private DatabasePopulator databasePopulator;

    public LocalAnnotationSessionFactory()
    {
        eventListeners = new EventListeners(null);
        super.setEventListeners(eventListeners.listeners());
    }

    @Override
    public void setEventListeners(final Map<String, Object> listeners)
    {
        this.eventListeners = new EventListeners(listeners);
        Map<String, Object> map = this.eventListeners.listeners();
        log.debug("setting client's(wrapped with internal) event_listeners = %s", map);
        super.setEventListeners(map);
    }

    @Override
    protected void afterSessionFactoryCreation() throws Exception
    {
        super.afterSessionFactoryCreation();
        if (eventListeners != null) { eventListeners.initialize(getConfiguration()); }
        JdbcTemplate jdbcTemplate = new JdbcTemplate(getDataSource());
        jdbcTemplate.execute(new ConnectionCallback<Object>()
        {
            @Override
            public Object doInConnection(Connection connection) throws SQLException, DataAccessException
            {
                Dialect dialect = Dialect.getDialect(getConfiguration().getProperties());
                DatabaseMetadata metaData = new DatabaseMetadata(connection, dialect);
                try {
                    String hibernateScriptsFolder = path(HIBERNATE_DATA_DIR, "scripts");
                    String timestamp = Long.valueOf(System.currentTimeMillis()).toString();
                    String updateOutput = path(hibernateScriptsFolder, "schema_update", timestamp + ".sql");
                    String schemaCreationOutput = path(hibernateScriptsFolder, "schema_create", timestamp + ".sql");
                    String schemaDropOutput = path(hibernateScriptsFolder, "schema_drop", timestamp + ".sql");
                    //
                    if (generateSchemaUpdateScript) {
                        File file = new File(updateOutput);
                        writeStringToFile(file, join(getConfiguration().generateSchemaUpdateScript(dialect, metaData), LINE_SEPARATOR));
                        log.info("update schema script dumped to %s ", file.getCanonicalPath());
                    }
                    if (generateSchemaCreateScript) {
                        File file = new File(schemaCreationOutput);
                        writeStringToFile(file, join(getConfiguration().generateSchemaCreationScript(dialect), LINE_SEPARATOR));
                        log.info("create schema script dumped to %s ", file.getCanonicalPath());
                    }
                    if (generateSchemaDropScript) {
                        File file = new File(schemaDropOutput);
                        writeStringToFile(file, join(getConfiguration().generateDropSchemaScript(dialect), LINE_SEPARATOR));
                        log.info("drop schema script dumped to %s ", file.getCanonicalPath());
                    }
                }
                catch (final IOException e) {
                    log.error(e);
                }
                return null;
            }
        });
    }

    @Override
    protected void postProcessConfiguration(Configuration config) throws HibernateException
    {
        super.postProcessConfiguration(config);
        //
        if (getHibernateProperties().contains(Environment.TRANSACTION_STRATEGY)) {
            log.debug("transaction_strategy changed from %s to %s", config.getProperty(Environment.TRANSACTION_STRATEGY),
                      getHibernateProperties().getProperty(Environment.TRANSACTION_STRATEGY));
            config.setProperty(Environment.TRANSACTION_STRATEGY, getHibernateProperties().getProperty(Environment.TRANSACTION_STRATEGY));
        }
    }

    @Override
    public void updateDatabaseSchema() throws DataAccessException
    {
        if (lockProvider != null) {
            log.info("using lockProvider %s for db schema update", lockProvider);
            Lock lock = lockProvider.lockFor(RTTP_DB_UPDATE_LOCK);
            try {
                if (lock.tryLock() && !lockProvider.isWorkDone()) {
                    updateDatabaseSchemaBeforeHibernate();
                    super.updateDatabaseSchema();
                    lockProvider.markWorkDone();
                    log.info("db schema update done using lock %s", lock);
                }
            }
            catch (DataAccessException e) {
                logger.error(e);
                throw e;
            }
            finally {
                lock.unlock();
            }
        }
        else {
            updateDatabaseSchemaBeforeHibernate();
            super.updateDatabaseSchema();
        }
    }

    protected void updateDatabaseSchemaBeforeHibernate()
    {
        if (databasePopulator != null) {
            JdbcTemplate jdbcTemplate = new JdbcTemplate(getDataSource());
            jdbcTemplate.execute(new ConnectionCallback<Object>()
            {
                @Override
                public Object doInConnection(Connection con) throws SQLException, DataAccessException
                {
                    log.info("performing db population using %s and connection = %s before hibernate schema update", databasePopulator, con);
                    try {
                        databasePopulator.populate(con);
                    }
                    catch (SQLException e) {
                        log.error(e);
                    }
                    return null;
                }
            });
        }
    }

    /**
     * for clustered env clients probably would like to run schema update only from one server at the same time, because parallel schema update can cause
     * dead-locks or some other issue(even performance issues). It would be great to run schema update once and from one server.
     *
     * @param lockProvider distributed lock provider
     */
    public void setLockProvider(IDistributedLockProvider lockProvider)
    {
        this.lockProvider = lockProvider;
    }

    /**
     * enable/disable schema creation script generation logic.
     * <p/>
     * script will be stored into <code>./data/schema_update/System.currentTimeMillis.sql</code> file by default.
     * <p/>
     * NOTE : if this feature enabled please ensure directory ./data/schema_update/ is writable.
     *
     * @param generateSchemaUpdateScript true/false flag.
     */
    public void setGenerateSchemaUpdateScript(boolean generateSchemaUpdateScript)
    {
        this.generateSchemaUpdateScript = generateSchemaUpdateScript;
    }

    /**
     * enable/disable schema update script generation logic.
     * <p/>
     * script will be stored into <code>./data/schema_update/System.currentTimeMillis.sql</code> file by default.
     * <p/>
     * NOTE : if this feature enabled please ensure directory ./data/schema_update/ is writable.
     *
     * @param generateSchemaCreateScript true/false flag.
     */
    public void setGenerateSchemaCreateScript(boolean generateSchemaCreateScript)
    {
        this.generateSchemaCreateScript = generateSchemaCreateScript;
    }

    /**
     * enable/disable schema update script generation logic.
     * <p/>
     * script will be stored into <code>./data/schema_drop/System.currentTimeMillis.sql</code> file by default.
     * <p/>
     * NOTE : if this feature enabled please ensure directory ./data/schema_drop/ is writable.
     *
     * @param generateSchemaDropScript true/false flag.
     */
    public void setGenerateSchemaDropScript(boolean generateSchemaDropScript)
    {
        this.generateSchemaDropScript = generateSchemaDropScript;
    }

    /**
     * allows to inject pre-hibernate-schema-creation db populator. Clients may want to perform additional db population before hibernate will create/update
     * database(in case of db.update enabled)
     *
     * @param databasePopulator populator
     */
    public void setDatabasePopulator(DatabasePopulator databasePopulator)
    {
        this.databasePopulator = databasePopulator;
    }

    /**
     * set client properties. also there is a small enrichment around hibernate envers and hibernate search(setting default values).
     *
     * @param hibernateProperties client properties.
     */
    @Override
    public void setHibernateProperties(Properties hibernateProperties)
    {
        if (!hibernateProperties.contains("hibernate.search.default.indexBase")) {
            hibernateProperties.put("hibernate.search.default.indexBase", HIBERNATE_SEARCH_DATA_DIR);
        }
        if (!hibernateProperties.contains("hibernate.search.default.directory_provider")) {
            hibernateProperties.put("hibernate.search.default.directory_provider", FSDirectoryProvider.class.getName());
        }
        if (!hibernateProperties.contains("org.hibernate.envers.do_not_audit_optimistic_locking_field")) {
            hibernateProperties.put("org.hibernate.envers.do_not_audit_optimistic_locking_field", FALSE.toString());
        }
        if (!hibernateProperties.contains("org.hibernate.envers.store_data_at_delete")) {
            hibernateProperties.put("org.hibernate.envers.store_data_at_delete", TRUE.toString());
        }
        hibernateProperties.put("hibernate.search.autoregister_listeners", FALSE.toString());
        super.setHibernateProperties(hibernateProperties);
    }

    @Override
    public void destroy() throws HibernateException
    {
        try {
            eventListeners.cleanup();
        }
        finally {
            super.destroy();
        }
    }

    /**
     * schema update script was generated?
     *
     * @return true if schema update script was generated during spring application context initialization.
     */
    public boolean isGenerateSchemaUpdateScript()
    {
        return generateSchemaUpdateScript;
    }

    /**
     * schema create script was generated?
     *
     * @return true if schema create script was generated during spring application context initialization.
     */
    public boolean isGenerateSchemaCreateScript()
    {
        return generateSchemaCreateScript;
    }

    /**
     * schema delete script was generated?
     *
     * @return true if schema delete script was generated during spring application context initialization.
     */
    public boolean isGenerateSchemaDropScript()
    {
        return generateSchemaDropScript;
    }
}
